//
// PhysicsComponent.cs
//
// This file is part of the QuickStart Engine. See http://www.codeplex.com/QuickStartEngine
// for license details.
//

using System;
using System.Collections.Generic;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;

namespace Alex_s_Dream_The_Toy_World
{
    /// <summary>
    /// Lets a PhysicsComponent know which type of shape is referenced.
    /// </summary>
    public enum ShapeType
    {
        Invalid = -1,
        Box = 0,
        Sphere,
        Capsule,        
        Heightfield,
        TriangleMesh,
    }

    /// <summary>
    /// Creates a physics component, which holds the physics actor for an entity to be used with the PhysicsManager.
    /// </summary>
    public class PhysicsComponent : BaseComponent
    {
        /// <summary>
        /// Physics actor holds all the information about the physics object.
        /// </summary>
        public IPhysicsActor PhysicsActor
        {
            get { return this.actor; }
        }
        protected IPhysicsActor actor;

        /// <summary>
        /// This components <seealso cref="ShapeType"/>
        /// </summary>
        public ShapeType ShapeType
        {
            get { return this.shapeType; }
        }
        protected ShapeType shapeType;

        /// <summary>
        /// Whether or not this entity's physics are dynamic or not.
        /// </summary>
        public bool IsDynamic
        {
            get { return this.isDynamic; }
        }
        protected bool isDynamic;

        /// <summary>
        /// Density of this entity's physics.
        /// </summary>
        public float Density
        {
            get { return this.density; }
        }
        protected float density;

        //@TODO: StaticModel has a lot of extra info made for rendering, we need PhysicsModel which would
        //       require less memory and load faster.
        public StaticModel PhysMesh
        {
            get { return physMesh; }
        }
        protected StaticModel physMesh;

        /// <summary>
        /// Sets the physics mesh's position
        /// </summary>
        /// <param name="Position">Position to set physics to</param>
        public void SetPosition( Vector3 Position )
        {
            this.actor.Position = Position;
        }

        /// <summary>
        /// Sets the physics mesh's rotation
        /// </summary>
        /// <param name="Rotation">Rotation to set physics to</param>
        public void SetRotation( Matrix Rotation )
        {
            this.actor.Orientation = Rotation;
        } 

        /// <summary>
        /// Create a physics component
        /// </summary>
        /// <param name="parent">Entity this component will be attached to</param>
        /// <param name="type">Type of shape this object will be</param>
        /// <param name="density">Density of physics object</param>
        /// <param name="isDynamic">Whether or not the physics will be dynamic (respond to gravity and collision)</param>
        public PhysicsComponent(BaseEntity parent, ShapeType type, float density, bool isDynamic)
            : base(parent)
        {
            ActivateComponent();

            this.shapeType = type;
            this.density = density;
            this.isDynamic = isDynamic;

            InitializeActor();
        }

        /// <summary>
        /// Create a physics component
        /// </summary>
        /// <param name="parent">Entity this component will be attached to</param>
        /// <param name="physicsModelPath">Path of model to use for physics mesh</param>
        /// <param name="density">Density of physics object</param>
        /// <param name="isDynamic">Whether or not the physics will be dynamic (respond to gravity and collision)</param>
        /// <remarks>If you want to use the same model for physics as you're using for rendering, then use the constructor that
        /// requires a 'ShapeType' for input, and use ShapeType.TriangleMesh.</remarks>
        public PhysicsComponent(BaseEntity parent, string physicsModelPath, float density, bool isDynamic)
            : base(parent)
        {
            this.shapeType = ShapeType.TriangleMesh;
            this.density = density;
            this.isDynamic = isDynamic;

            this.physMesh = this.parentEntity.Game.StaticModelLoader.LoadStaticModel(physicsModelPath);

            InitializeActor();
        }

        /// <summary>
        /// Create a physics component for terrain
        /// </summary>
        /// <param name="parent">Entity this component will be attached to</param>
        /// <param name="heightData">Height of all vertices of the heightmap</param>
        /// <param name="scaleFactor">Scaling value of vertices</param>
        public PhysicsComponent(BaseEntity parent, float [,] heightData, int scaleFactor)
            : base(parent)
        {
            this.shapeType = ShapeType.Heightfield;

            MsgGetPhysicsScene getPhysSceneMsg = ObjectPool.Aquire<MsgGetPhysicsScene>();
            getPhysSceneMsg.UniqueTarget = this.parentEntity.UniqueID;
            this.parentEntity.Game.SendInterfaceMessage(getPhysSceneMsg, InterfaceType.Physics);

            IPhysicsScene physScene = getPhysSceneMsg.PhysicsScene;
            if (physScene != null)
            {
                CreateHeightfieldActor(physScene, heightData, scaleFactor);

                if (this.actor != null)
                {
                    MsgAddEntityToPhysicsScene addToSceneMsg = ObjectPool.Aquire<MsgAddEntityToPhysicsScene>();
                    addToSceneMsg.Actor = this.actor;
                    addToSceneMsg.EntityID = this.parentEntity.UniqueID;
                    this.parentEntity.Game.SendInterfaceMessage(addToSceneMsg, InterfaceType.Physics);
                }
            }
        }


        public override void Shutdown()
        {
            base.Shutdown();

            // We need to tell the physics interface to remove this entity's physics from the world
            MsgRemoveEntityFromPhysicsScene remFromSceneMsg = ObjectPool.Aquire<MsgRemoveEntityFromPhysicsScene>();
            remFromSceneMsg.EntityID = this.parentEntity.UniqueID;
            this.parentEntity.Game.SendInterfaceMessage(remFromSceneMsg, InterfaceType.Physics);
        }


        protected virtual void InitializeActor()
        {
            MsgGetPhysicsScene getPhysSceneMsg = ObjectPool.Aquire<MsgGetPhysicsScene>();            
            getPhysSceneMsg.UniqueTarget = this.parentEntity.UniqueID;
            this.parentEntity.Game.SendInterfaceMessage(getPhysSceneMsg, InterfaceType.Physics);

            IPhysicsScene physScene = getPhysSceneMsg.PhysicsScene;
            if (physScene != null)
            {
                CreateActor(physScene);

                if (this.actor != null)
                {
                    MsgAddEntityToPhysicsScene addToSceneMsg = ObjectPool.Aquire<MsgAddEntityToPhysicsScene>();
                    addToSceneMsg.Actor = this.actor;
                    addToSceneMsg.EntityID = this.parentEntity.UniqueID;
                    this.parentEntity.Game.SendInterfaceMessage(addToSceneMsg, InterfaceType.Physics);                    
                }
            }
        }

        /// <summary>
        /// Update the component
        /// </summary>
        /// <param name="gameTime">XNA Timing snapshot</param>
        public override void Update(GameTime gameTime)
        {
            if (this.actor != null)
            {
                this.parentEntity.UpdateFromPhysics(this.actor.Position, this.actor.Orientation);
            }

            this.actor.UpdateCollisions();
        }

        /// <summary>
        /// Creates an actor using shape info and information from the parent BaseEntity.
        /// </summary>
        /// <param name="PhysicsScene">Reference to the physics scene</param>
        protected virtual void CreateActor(IPhysicsScene PhysicsScene)
        {
            ShapeDesc newShape = CreateShapeFromType(shapeType);

            if (newShape == null)
            {
                throw new Exception("Shape did not load properly");
            }

            ActorDesc desc = new ActorDesc();
            desc.Orientation = this.parentEntity.Rotation;
            desc.Density = this.density;
            desc.Dynamic = this.isDynamic;
            desc.Position = this.parentEntity.Position;
            desc.Shapes.Add(newShape);
            desc.EntityID = this.parentEntity.UniqueID;
            desc.Game = this.parentEntity.Game;
            desc.Phantom = false;
            desc.Character = false;

            this.actor = PhysicsScene.CreateActor(desc);
        }

        /// <summary>
        /// Creates an actor using shape info and information from the parent BaseEntity.
        /// </summary>
        /// <param name="PhysicsScene">Reference to the physics scene</param>
        /// <param name="density">Density of physics object</param>
        private void CreateHeightfieldActor(IPhysicsScene PhysicsScene, float[,] heightData, float scaleFactor)
        {
            HeightFieldShapeDesc heightfieldShape = CreateHeightfieldShape(heightData, scaleFactor);

            if (heightfieldShape == null)
            {
                throw new Exception("Shape did not load properly");
            }

            ActorDesc desc = new ActorDesc();
            desc.Orientation = parentEntity.Rotation;
            desc.Density = 0.0f;
            desc.Dynamic = false;
            desc.Position = parentEntity.Position;
            desc.EntityID = this.parentEntity.UniqueID;
            desc.Shapes.Add(heightfieldShape);

            actor = PhysicsScene.CreateActor(desc);
        }

        /// <summary>
        /// Create a shape from a <seealso cref="ShapeType"/> and the BaseEntity information.
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        protected ShapeDesc CreateShapeFromType(ShapeType type)
        {
            float scale = this.parentEntity.Scale;

            switch (type)
            {
                case ShapeType.Box:
                    {
                        BoxShapeDesc shape = new BoxShapeDesc();
                        shape.Extents = new Vector3(scale, scale, scale);
                        return shape;
                    }
                case ShapeType.Sphere:
                    {
                        SphereShapeDesc shape = new SphereShapeDesc();
                        shape.Radius = scale;
                        return shape;
                    }
                case ShapeType.Heightfield:
                    {
                        // Unsupported by this method, use CreateHeightfieldShape()
                        return null;
                    }
                case ShapeType.Capsule:
                    {
                        CapsuleShapeDesc shape = new CapsuleShapeDesc();
                        shape.Radius = scale;
                        shape.Length = scale;
                        return shape;
                    }
                case ShapeType.TriangleMesh:
                    {
                        if (isDynamic)
                        {
                            throw new Exception("Triangle Mesh shapes do not support dynamic physics");
                        }

                        TriangleMeshShapeDesc shape = new TriangleMeshShapeDesc();

                        if (physMesh == null)
                        {
                            MsgGetModelVertices msgGetVerts = ObjectPool.Aquire<MsgGetModelVertices>();                            
                            msgGetVerts.UniqueTarget = this.parentEntity.UniqueID;
                            this.parentEntity.Game.SendMessage(msgGetVerts);

                            shape.Vertices = msgGetVerts.Vertices;

                            MsgGetModelIndices msgGetInds = ObjectPool.Aquire<MsgGetModelIndices>();
                            msgGetInds.UniqueTarget = this.parentEntity.UniqueID;
                            this.parentEntity.Game.SendMessage(msgGetInds);

                            shape.Indices = msgGetInds.Indices;
                        }
                        else
                        {
                            shape.Vertices = physMesh.GetModelVertices(this.parentEntity.Scale);
                            shape.Indices = physMesh.GetModelIndices();
                        }

                        if ((shape.Vertices.Count == 0) || (shape.Indices.Count == 0))
                            return null;

                        return shape;
                    }
                default:
                    // Throw exception
                    return null;
            };
        }

        /// <summary>
        /// Creates a heightfield shape descriptor for the physics system.
        /// </summary>
        /// <param name="heightData">Array of all height values on the heightfield</param>
        /// <param name="scaleFactor">Terrain scale factor. 4x means a spacing of 4 units (along horz plane) between adjacent vertices</param>
        /// <returns>Heightfield shape descriptor.</returns>
        private HeightFieldShapeDesc CreateHeightfieldShape(float[,] heightData, float scaleFactor)
        {
            HeightFieldShapeDesc heightFieldDesc = new HeightFieldShapeDesc();
            heightFieldDesc.HeightField = heightData;
            heightFieldDesc.SizeX = heightData.GetLength(0) * scaleFactor;
            heightFieldDesc.SizeZ = heightData.GetLength(1) * scaleFactor;

            return heightFieldDesc;
        }

        /// <summary>
        /// Handles a message sent to this component.
        /// </summary>
        /// <param name="message">Message to be handled</param>
        /// <returns>True, if handled, otherwise false</returns>
        public override bool ExecuteMessage(IMessage message)
        {
            if (message.UniqueTarget != this.parentEntity.UniqueID)
                return false;

            switch (message.Type)
            {
                case MessageType.SetPosition:
                    {
                        MsgSetPosition msgSetPos = message as MsgSetPosition;
                        message.TypeCheck(msgSetPos);

                        SetPosition(msgSetPos.Position);
                    }
                    return true;
                case MessageType.ModifyPosition:
                    {
                        MsgModifyPosition msgModPos = message as MsgModifyPosition;
                        message.TypeCheck(msgModPos);

                        SetPosition(this.actor.Position + msgModPos.Position);
                    }
                    return true;                
                case MessageType.SetRotation:
                    {
                        MsgSetRotation setRotMsg = message as MsgSetRotation;
                        message.TypeCheck(setRotMsg);

                        SetRotation(setRotMsg.Rotation);
                    }
                    return true;
                case MessageType.ModifyRotation:
                    {
                        MsgModifyRotation modRotMsg = message as MsgModifyRotation;
                        message.TypeCheck(modRotMsg);

                        SetRotation(this.actor.Orientation *= modRotMsg.Rotation);
                    }
                    return true;
                case MessageType.SetLinearVelocity:
                    {
                        MsgSetLinearVelocity setLinVelMsg = message as MsgSetLinearVelocity;
                        message.TypeCheck(setLinVelMsg);

                        this.actor.LinearVelocity = setLinVelMsg.LinearVelocity;
                    }
                    return true;
                case MessageType.GetLinearVelocity:
                    {
                        MsgGetLinearVelocity getLinVelMsg = message as MsgGetLinearVelocity;
                        message.TypeCheck(getLinVelMsg);

                        getLinVelMsg.LinearVelocity = this.actor.LinearVelocity;
                    }
                    return true;
                case MessageType.SetAngularVelocity:
                    {
                        MsgSetAngularVelocity setAngVelMsg = message as MsgSetAngularVelocity;
                        message.TypeCheck(setAngVelMsg);

                        this.actor.AngularVelocity = setAngVelMsg.AngularVelocity;
                    }
                    return true;
                case MessageType.GetAngularVelocity:
                    {
                        MsgGetAngularVelocity getAngVelMsg = message as MsgGetAngularVelocity;
                        message.TypeCheck(getAngVelMsg);

                        getAngVelMsg.AngularVelocity = this.actor.AngularVelocity;
                    }
                    return true;
                case MessageType.AddLinearForce:
                    {
                        MsgAddLinearForce addForceMsg = message as MsgAddLinearForce;
                        message.TypeCheck(addForceMsg);

                        if (addForceMsg.LinearVelocity.LengthSquared() > 0.0f)
                        {
                            this.actor.LinearVelocity += addForceMsg.LinearVelocity;
                        }
                    }
                    return true;
                case MessageType.AddAngularForce:
                    {
                        MsgAddAngularForce addForceMsg = message as MsgAddAngularForce;
                        message.TypeCheck(addForceMsg);

                        this.actor.AngularVelocity += addForceMsg.AngularVelocity;
                    }
                    return true;
                case MessageType.ListenForCollision:
                    {
                        MsgListenForCollision msgListenColl = message as MsgListenForCollision;
                        message.TypeCheck(msgListenColl);

                        if (msgListenColl.ListenForCollisions == true)
                        {
                            this.actor.EnableCollisionListening();
                        }
                        else
                        {
                            this.actor.DisableCollisionListening();
                        }
                    }
                    return true;
                case MessageType.GetHasDynamicPhysics:
                    {
                        MsgGetHasDynamicPhysics msgGetDynPhys = message as MsgGetHasDynamicPhysics;
                        message.TypeCheck(msgGetDynPhys);

                        msgGetDynPhys.HasDynamicPhysics = this.isDynamic;
                    }
                    return true;
                case MessageType.SetPhysicsMovableState:
                    {
                        MsgSetPhysicsMovableState msgSetDynPhys = message as MsgSetPhysicsMovableState;
                        message.TypeCheck(msgSetDynPhys);

                        if ( this.actor == null )
                            return false;

                        this.actor.SetMovable(msgSetDynPhys.Movable);

                        this.isDynamic = msgSetDynPhys.Movable;
                    }
                    return true;
                default:
                    return false;
            }
        }

        /// <summary>
        /// Allows the renderer to query this component for render information.
        /// </summary>
        /// <param name="desc">Descriptor reference from the renderer</param>
        public override void QueryForChunks(ref RenderPassDesc desc)
        {
        }
    }
}
